/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

#define fopen	__real_fopen
#define fclose	__real_fclose
#define fgets	__real_fgets
#define lseek	__real_lseek
#define close	__real_close
#include <unistd.h>
#include <stdio.h>
#undef fopen
#undef fclose
#undef fgets
#undef lseek
#undef close

#include "zbxmocktest.h"
#include "zbxmockdata.h"
#include "zbxmockutil.h"

#include "zbxcommon.h"
#include "zbxalgo.h"

#define ZBX_MOCK_MAX_FILES	16

void	*mock_streams[ZBX_MOCK_MAX_FILES];

static zbx_mock_handle_t	fragments;

static FILE	*(*fopen_mock_callback)(const char *, const char *) = NULL;

struct zbx_mock_IO_FILE
{
	const char	*contents;
};

FILE	*__wrap_fopen(const char *path, const char *mode);
int	__wrap_fclose(FILE *fp);
char	*__wrap_fgets(char *s, int size, FILE *stream);
int	__wrap_fstat(int __fildes, struct stat *__stat_buf);
int	__wrap_connect(int socket, void *addr, socklen_t address_len);
int	__wrap_poll(struct pollfd *pds, int nfds, int timeout);
off_t	__wrap_lseek(int fd, off_t offset, int whence);
int	__wrap_close(int fd);
ssize_t	__wrap_read(int fildes, void *buf, size_t nbyte);
int	__wrap_open(const char *path, int oflag, ...);
int	__wrap_stat(const char *path, struct stat *buf);
int	__wrap___xstat(int ver, const char *pathname, struct stat *buf);
#ifdef HAVE_FXSTAT
int	__wrap___fxstat(int __ver, int __fildes, struct stat *__stat_buf);
#endif

int	__real_open(const char *path, int oflag, ...);
int	__real_stat(const char *path, struct stat *buf);
int	__real_fstat(int __fildes, struct stat *__stat_buf);
#ifdef HAVE_FXSTAT
int	__real___fxstat(int __ver, int __fildes, struct stat *__stat_buf);
#endif

static int	is_profiler_path(const char *path)
{
	size_t	len;

	len = strlen(path);

	if ((ZBX_CONST_STRLEN(".gcda") < len && 0 == strcmp(path + len - ZBX_CONST_STRLEN(".gcda"), ".gcda")) ||
			(ZBX_CONST_STRLEN(".gcno") < len &&
					0 == strcmp(path + len - ZBX_CONST_STRLEN(".gcno"), ".gcno")))
	{
		return SUCCEED;
	}

	return FAIL;
}

static int	is_mock_stream(FILE *stream)
{
	int	i;

	for (i = 0; i < ZBX_MOCK_MAX_FILES && NULL != mock_streams[i]; i++)
	{
		if (stream == mock_streams[i])
			return SUCCEED;
	}

	return FAIL;
}

FILE	*__wrap_fopen(const char *path, const char *mode)
{
	zbx_mock_error_t	error;
	zbx_mock_handle_t	file_contents;
	const char		*contents;
	struct zbx_mock_IO_FILE	*file = NULL;

	/* in case a test needs a custom fopen mock, use callback instead */
	if (NULL != fopen_mock_callback)
		return (*fopen_mock_callback)(path, mode);

	if (SUCCEED == is_profiler_path(path))
		return __real_fopen(path, mode);

	if (0 != strcmp(mode, "r"))
	{
		fail_msg("fopen() modes other than \"r\" are not supported.");
	}
	else if (ZBX_MOCK_NO_PARAMETER == (error = zbx_mock_file(path, &file_contents)))
	{
		errno = ENOENT;	/* No such file or directory */
	}
	else if (ZBX_MOCK_SUCCESS != error)
	{
		fail_msg("Error while trying to open path \"%s\" from test case data: %s", path,
				zbx_mock_error_string(error));
	}
	else if (ZBX_MOCK_SUCCESS != (error = zbx_mock_string(file_contents, &contents)))
	{
		fail_msg("Error while trying to get contents of file \"%s\" from test case data: %s", path,
				zbx_mock_error_string(error));
	}
	else
	{
		int	i;

		for (i = 0; i < ZBX_MOCK_MAX_FILES && NULL != mock_streams[i]; i++)
			;

		if (i < ZBX_MOCK_MAX_FILES)
		{
			file = zbx_malloc(file, sizeof(struct zbx_mock_IO_FILE));
			file->contents = contents;
			mock_streams[i] = file;
		}
		else
			errno = EMFILE;	/* The per-process limit on the number of open file descriptors has been reached */
	}

	return (FILE *)file;
}

int	__wrap_fclose(FILE *fp)
{
	if (SUCCEED != is_mock_stream(fp))
		return __real_fclose(fp);

	zbx_free(fp);
	return 0;
}

char	*__wrap_fgets(char *s, int size, FILE *stream)
{
	struct zbx_mock_IO_FILE	*file = (struct zbx_mock_IO_FILE *)stream;
	int			length;
	const char		*newline;

	if (SUCCEED != is_mock_stream(stream))
		return __real_fgets(s, size, stream);

	assert_non_null(s);
	assert_true(0 < size);

	if ('\0' == *file->contents)
		return NULL;

	if (size - 1 < (length = strlen(file->contents)))
		length = size - 1;

	if (NULL != (newline = strchr(file->contents, '\n')) && newline - file->contents + 1 < length)
		length = newline - file->contents + 1;

	assert_int_equal(length, zbx_snprintf(s, size, "%.*s", length, file->contents));
	file->contents += length;
	return s;
}

int	__wrap_connect(int socket, void *addr, socklen_t address_len)
{
	zbx_mock_error_t	error;

	ZBX_UNUSED(socket);
	ZBX_UNUSED(addr);
	ZBX_UNUSED(address_len);

	if (ZBX_MOCK_SUCCESS != (error = zbx_mock_in_parameter("fragments", &fragments)))
		fail_msg("Cannot get fragments handle: %s", zbx_mock_error_string(error));

	return 0;
}

#define MOCK_POLL_DEFAULT		0
#define MOCK_POLL_TIMEOUT		1
#define MOCK_POLL_ERROR			2
#define MOCK_POLL_REVENTS_ERROR		3
#define MOCK_POLL_SOCKET_BLOCKING_ERROR	4

static int	g_mock_poll_mode = MOCK_POLL_DEFAULT;

void	zbx_comms_mock_poll_set_mode_from_param(const char *param)
{
	if (0 == strcmp(param, "MOCK_POLL_TIMEOUT"))
		g_mock_poll_mode = MOCK_POLL_TIMEOUT;
	else if (0 == strcmp(param, "MOCK_POLL_ERROR"))
		g_mock_poll_mode = MOCK_POLL_ERROR;
	else if (0 == strcmp(param, "MOCK_POLL_REVENTS_ERROR"))
		g_mock_poll_mode = MOCK_POLL_REVENTS_ERROR;
	else if (0 == strcmp(param, "MOCK_POLL_SOCKET_BLOCKING_ERROR"))
		g_mock_poll_mode = MOCK_POLL_SOCKET_BLOCKING_ERROR;
	else
		g_mock_poll_mode = MOCK_POLL_DEFAULT;
}

int	__wrap_poll(struct pollfd *pds, int nfds, int timeout)
{
	ZBX_UNUSED(timeout);

	switch (g_mock_poll_mode)
	{
		case MOCK_POLL_TIMEOUT:
			return 0;
		case MOCK_POLL_ERROR:
			return -1;
		case MOCK_POLL_REVENTS_ERROR:
			for (int i = 0; i < nfds; i++)
				pds[i].revents = POLLERR;
			return nfds;
		case MOCK_POLL_SOCKET_BLOCKING_ERROR:
			errno = EPERM;
			return -1;
		case MOCK_POLL_DEFAULT:
		default:
			for (int i = 0; i < nfds; i++)
				pds[i].revents = (POLLIN | POLLOUT);
			return nfds;
	}
}

#undef MOCK_POLL_DEFAULT
#undef MOCK_POLL_TIMEOUT
#undef MOCK_POLL_ERROR
#undef MOCK_POLL_REVENTS_ERROR
#undef MOCK_POLL_SOCKET_BLOCKING_ERROR

static const char	*frag_data = NULL;
static const char	*frag_pos = NULL;
static size_t		frag_sz = 0;
static int		zbx_comms_test_comms = FAIL;
static int		zbx_comms_read_iter = 0;
static int		zbx_comms_read_ret[5];

static int	next_fragment(void)
{
	zbx_mock_handle_t	fragment;
	zbx_mock_error_t	error;

	if (ZBX_MOCK_SUCCESS != zbx_mock_vector_element(fragments, &fragment))
		return 0;	/* no more data */

	if (ZBX_MOCK_SUCCESS != (error = zbx_mock_binary(fragment, &frag_data, &frag_sz)))
		fail_msg("Cannot read data '%s'", zbx_mock_error_string(error));
	frag_pos = frag_data;

	return 1;
}

int	__wrap_open(const char *path, int oflag, ...)
{
	if (SUCCEED == is_profiler_path(path))
	{
		va_list	args;
		int	fd;

		va_start(args, oflag);
		fd = __real_open(path, oflag, va_arg(args, int));
		va_end(args);
		return fd;
	}

	fragments = zbx_mock_get_parameter_handle("in.fragments");
	next_fragment();

	return INT_MAX;
}

/******************************************************************************
 *                                                                            *
 * Comments: Note that simply wrapping read function will break any compiled  *
 *           in tool, that would attempt to use read() function. In this case *
 *           some safeguards must be added to implement pass-through          *
 *           functionality like it's done with open/fxstat etc functions for  *
 *           coverage builds.                                                 *
 *                                                                            *
 ******************************************************************************/
ssize_t	__wrap_read(int fildes, void *buf, size_t nbyte)
{
	size_t	mv_len;

	ZBX_UNUSED(fildes);

	if (SUCCEED == zbx_comms_test_comms)
	{
		int	ret = zbx_comms_read_ret[zbx_comms_read_iter];
		zbx_comms_read_iter++;

		return ret;
	}

	if (frag_pos >= frag_data + frag_sz)
	{
		if (1 != next_fragment())
			return 0;
	}

	if ((ssize_t)nbyte < (frag_data + frag_sz) - frag_pos)
		mv_len = nbyte;
	else
		mv_len = (frag_data + frag_sz) - frag_pos;

	memcpy(buf, frag_pos, mv_len);

	frag_pos += mv_len;

	return mv_len;
}

void	zbx_comms_set_test_comms(int status)
{
	zbx_comms_test_comms = status;
}

void	zbx_comms_setup_read(zbx_vector_int32_t *v)
{
	for (int i = 0; i < v->values_num; i++)
		zbx_comms_read_ret[i] = v->values[i];
}

off_t	__wrap_lseek(int fd, off_t offset, int whence)
{
	const char	*new_pos;

	if (fd != INT_MAX)
		return __real_lseek(fd, offset, whence);

	switch(whence)
	{
		case SEEK_END:
			new_pos = (frag_data + frag_sz) + offset;
			break;
		case SEEK_CUR:
			new_pos = frag_pos + offset;
			break;
		case SEEK_SET:
			new_pos = frag_data + offset;
			break;
		default:
			errno = EINVAL;
			goto error;
	}

	if (new_pos < frag_data || new_pos > frag_data + frag_sz)
	{
		errno = EOVERFLOW;
		goto error;
	}

	frag_pos = new_pos;

	return frag_pos - frag_data;
error:
	return (off_t)-1;
}

int	__wrap_close(int fd)
{
	if (fd != INT_MAX) return __real_close(fd);

	frag_data = frag_pos = NULL;
	frag_sz = 0;

	return 0;
}

int	__wrap_stat(const char *path, struct stat *buf)
{
	zbx_mock_error_t	error;
	zbx_mock_handle_t	handle;

	if (SUCCEED == is_profiler_path(path))
		return __real_stat(path, buf);

	if (ZBX_MOCK_SUCCESS == (error = zbx_mock_file(path, &handle)))
	{
		buf->st_mode = S_IFMT & S_IFREG;
		return 0;
	}

	if (ZBX_MOCK_NO_PARAMETER != error)
		fail_msg("Error during path \"%s\" lookup among files: %s", path, zbx_mock_error_string(error));

	if (0)	/* directory lookup is not implemented */
	{
		buf->st_mode = S_IFMT & S_IFDIR;
		return 0;
	}

	errno = ENOENT;	/* No such file or directory */
	return -1;
}

int	__wrap_fstat(int __fildes, struct stat *__stat_buf)
{
	if (__fildes != INT_MAX)
		return __real_fstat(__fildes, __stat_buf);

	__stat_buf->st_size = zbx_mock_get_parameter_uint64("in.file_len");
	return 0;
}

int	__wrap___xstat(int ver, const char *pathname, struct stat *buf)
{
	ZBX_UNUSED(ver);

	if (SUCCEED == is_profiler_path(pathname))
		return __real_stat(pathname, buf);

	return __wrap_stat(pathname, buf);
}

#ifdef HAVE_FXSTAT
int	__wrap___fxstat(int __ver, int __fildes, struct stat *__stat_buf)
{
	if (__fildes != INT_MAX)
		return __real___fxstat(__ver, __fildes, __stat_buf);

	__stat_buf->st_size = zbx_mock_get_parameter_uint64("in.file_len");

	return 0;
}
#endif

void	zbx_set_fopen_mock_callback(FILE *(*fopen_callback)(const char *, const char *))
{
	fopen_mock_callback = fopen_callback;
}
